
import java.io.InputStream;
import java.io.OutputStream;
import java.util.PriorityQueue;
import java.util.Queue;
import javax.comm.CommPortIdentifier;
import javax.comm.SerialPort;

public class Paslezer implements Runnable {

  public static final byte APP_TRANSPORT = (byte) 0x82;
  public static final byte APP_MAG_STRIPE = (byte) 0x01;
  public static final byte[] MSG_BAUD_SYNC = {(byte) 0x01, (byte) 0x00, (byte) 0x96, (byte) 0x00, (byte) 0x02, (byte) 0x95, (byte) 0x4D, (byte) 0x54, (byte) 0x19};
  public static final byte[] MSG_RESYNCHRONIZE_CMD = {(byte) 0x01, (byte) 0x00, (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0x91, (byte) 0x00};
  public static final byte[] MSG_RESYNCHRONIZE_RSP = {(byte) 0x01, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0xA1, (byte) 0x00};
  Queue<byte[]> transmitQueue = new PriorityQueue<byte[]>();
  Queue<byte[]> responseQueue = new PriorityQueue<byte[]>();
  Queue<byte[]> notificationQueue = new PriorityQueue<byte[]>();
  public String portName = "COM7";
  public CommPortIdentifier portId;
  public SerialPort serialPort;
  public InputStream inputStream;
  public OutputStream outputStream;
  public int baudrate = 9600;
  public int databits = SerialPort.DATABITS_8;
  public int stopbits = SerialPort.STOPBITS_1;
  public int parity = SerialPort.PARITY_EVEN;
  public int rtscts_in = SerialPort.FLOWCONTROL_NONE;
  public int rtscts_out = SerialPort.FLOWCONTROL_NONE;
  public int xonxoff_in = SerialPort.FLOWCONTROL_NONE;
  public int xonxoff_out = SerialPort.FLOWCONTROL_NONE;
  public boolean debug = false;
  public static boolean consumed = false;

  public Paslezer() {
    try {
      portId = CommPortIdentifier.getPortIdentifier(portName);
      serialPort = (SerialPort) portId.open("MagTekReader ", 2000);
      inputStream = serialPort.getInputStream();
      outputStream = serialPort.getOutputStream();
      serialPort.setSerialPortParams(baudrate, databits, stopbits, parity);
      serialPort.setFlowControlMode(rtscts_in | rtscts_out | xonxoff_in | xonxoff_out);
      serialPort.setDTR(true);
      serialPort.notifyOnDataAvailable(true);
      Thread transmitting = new Thread(this, "MagTekApduPCO");
      transmitting.start();

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public String readTrack(byte n) {
//        byte[] tx = new byte[5];
    tx[0] = (byte) 0x00;
    tx[1] = APP_MAG_STRIPE;
    tx[2] = (byte) 0x82;
    tx[3] = (byte) 0x00;
    tx[4] = n;
    byte[] rx = transmit(tx);
    try {
      Thread.sleep(100);
    } catch (Exception err) {
    }
    try {
      switch (rx[3]) {
        case 0x00:
          return new String(rx);
        case 0x06:
          return "Bad track number";
        default:
          return "Power up of ICC failed";
      }
    } catch (Exception e) {
    }
    return "null";
  }

  public void consumeCard() {
    System.out.println("consumeCard");
    byte[] txs = new byte[4];
    txs[0] = (byte) 0x00;
    txs[1] = APP_TRANSPORT;
    txs[2] = (byte) 0x80;
    txs[3] = (byte) 0x00;
    byte[] rx = transmit(txs);
    try {
      Thread.sleep(100);
    } catch (Exception err) {
    }
    try {
      switch (rx[3]) {
        case 0x00:
          consumed = true;
          break;
        default:
          System.out.println("Unable to consume card");
      }
    } catch (Exception e) {
      System.out.println("not consumed" + e);
    }
  }

  public void ejectCard() {
    byte[] txs = new byte[4];
    txs[0] = (byte) 0x00;
    txs[1] = APP_TRANSPORT;
    txs[2] = (byte) 0x81;
    txs[3] = (byte) 0x00;
    byte[] rx = transmit(txs);
    try {
      switch (rx[3]) {
        case 0x00:
          consumed = false;
          break;
        default:
          System.out.println("Unable to eject card");
      }
    } catch (Exception e) {
      System.out.println("erro?");
    }
  }

  public byte[] transmit(byte[] data) {
    // System.out.println("transmit " +data);
    transmitQueue.add(data);
    synchronized (this) {
      notify();
    }
    long stoptime = System.currentTimeMillis() + 1500;
    do {
      try {
        byte[] response = responseQueue.remove();
        byte[] rx = response;
        if (rx[1] == data[1] && rx[2] == data[2]) {
          return rx;
        }
      } catch (Exception e) {
//e.printStackTrace();
      }
    } while (System.currentTimeMillis() < stoptime);
    //System.out.println("Unable to send data to card");
    return null;
  }
  public boolean interrupted = false;

  public void interrupt() {
    interrupted = true;
  }
  public static final int BWT = 250;
  public static final byte STATE_SA = 0x01;
  public static final byte STATE_DA = 0x02;
  public static final byte STATE_PCB = 0x03;
  public static final byte STATE_LEN1 = 0x04;
  public static final byte STATE_LEN2 = 0x05;
  public static final byte STATE_HEDC = 0x06;
  public static final byte STATE_DATA = 0x07;
  public static final byte STATE_FEDC = 0x08;
  public int state = STATE_SA;
  public static final byte PSTATE_SYNC = 0; // baud sync
  public static final byte PSTATE_INIT = 1; // resync
  public static final byte PSTATE_CONN = 2; // connected
  public int pstate = PSTATE_SYNC;
  public long rxacktime = 0;
  public long txacktime = 0;
  public boolean ack = false;
  public boolean poll = false;
  public boolean message = false;
  public boolean iFrameOutstanding = false;
  public byte iFrameOutstandingPcb = 0x00;
  public boolean sFrameOutstanding = false;
  public byte sFrameOutstandingPcb = 0x00;
  public int trials = 0;
  public byte[] tx = new byte[5];
  public int length = 0;
  public int checksum = 0;
  public int read = 0;
  public int errors = 0;
  public int txSequenceNumber = 0;
  public int rxSequenceNumber = 0;
  public byte pcb;
  public byte[] buffer = new byte[512];

  public void run() {
    try {
      long time = System.currentTimeMillis();
      long lastRx = time;
      while (!interrupted || ack || poll) {
        synchronized (this) {
          message = false;
          time = System.currentTimeMillis();
          if (inputStream.available() > 0) {
            lastRx = time;
          } else if (state != STATE_SA && (time - lastRx) > 3000) {
            sendError(pcb, (byte) 0x02);
            ack = false;
            poll = false;
            state = STATE_SA;
          }
          while (inputStream.available() > 0 && !message) {
            byte rx = (byte) inputStream.read();
//switch ((byte)(Math.random()*100))
            {
//case 0: break; // lost byte
//case 1: rx ^= 0xFF; break; // corrupted byte
//default:
              if (debug) {
                System.out.println(state + " " + System.currentTimeMillis() + " < " + rx);
              }
              switch (state) {
                case STATE_SA:
                  checksum = rx;
                  state = STATE_DA;
                  break;
                case STATE_DA:
                  checksum ^= rx;
                  state = STATE_PCB;
                  break;
                case STATE_PCB:
                  checksum ^= rx;
                  pcb = rx;
                  state = STATE_LEN1;
                  break;
                case STATE_LEN1:
                  checksum ^= rx;
                  length = rx & 0xff;
                  length *= 256;
                  state = STATE_LEN2;
                  break;
                case STATE_LEN2:
                  checksum ^= rx;
                  length += rx & 0xff;
                  if (length > buffer.length) {
                    buffer = new byte[length];
                  }
                  read = 0;
                  state = STATE_HEDC;
                  break;
                case STATE_HEDC:
                  if (rx != checksum) // System.out.println("HEDC error");
                  {
                    while (inputStream.available() > 0) {
                      rx = (byte) inputStream.read();
                      if (debug) {
                        System.out.println(state + " " + System.currentTimeMillis() + " < " + rx);
                      }
                    }
                    sendError(pcb, (byte) 0x01);
                    state = STATE_SA;
                  } else {
                    checksum ^= rx;
                    if (length > 0) {
                      state = STATE_DATA;
                    } else {
                      state = STATE_FEDC;
                    }
//switch (pcb & 0xC0)
//{
//case 0x00: // I-Frame
// if (iFrameOutstanding && txSequenceNumber != (pcb & 0x01)) // rule 4.8
// {
// txSequenceNumber = (txSequenceNumber + 1) % 2;
// iFrameOutstanding = false; // rule 4.9a
// transmitQueue.remove();
// errors = 0;
// }
// break;
//}
                  }
                  break;
                case STATE_DATA:
                  checksum ^= rx;
                  buffer[read++] = rx;
                  if (read == length) {
                    state = STATE_FEDC;
                  }
                  break;
                case STATE_FEDC:
                  if (rx == checksum) {
                    switch (pcb & 0xC0) {
                      case 0x00: // I-Frame
                        if ((pcb & 0x04) != 0x00) {
                          sendReject(pcb, (byte) 0x02); // reject frame - chaining not supported
                          break;
                        }
                        if (rxSequenceNumber == ((pcb & 0x02) >> 1)) // rule 4.4, 4.5
                        {
                          rxSequenceNumber = (rxSequenceNumber + 1) % 2;
                          switch (buffer[0]) {
                            case (byte) 0x40:
                              responseQueue.add(buffer);
                              break;
                            case (byte) 0x80:
                              notificationQueue.add(buffer);
                              break;
                          }
                        }
                        if (iFrameOutstanding && txSequenceNumber != (pcb & 0x01)) // rule 4.8
                        {
                          txSequenceNumber = (txSequenceNumber + 1) % 2;
                          iFrameOutstanding = false; // rule 4.9a
                          transmitQueue.remove();
                          errors = 0;
                        }
                        ack = true; // ack with R-Frame
                        rxacktime = System.currentTimeMillis() + 200;//100;//BWT;
                        pstate = PSTATE_CONN;
                        break;
                      case 0x80: // S-Frame
                        switch (pcb) {
                          case (byte) 0x85: // frame reject command (indication)
                            break;
                          case (byte) 0x88: // frame resend command (indication)
                            if (iFrameOutstanding && iFrameOutstandingPcb == buffer[7]) // rule 6.2b error recovery
                            {
                              iFrameOutstanding = false;
                              errors++;
                              if (errors > 2) {
                                pstate = PSTATE_INIT;
                              }
                            }
//else if (sFrameOutstanding && sFrameOutstandingPcb == buffer[7]); // rule 6.3a ignore
                            break;
                          case (byte) 0x90: // resynchronise protocol command
                            if (debug) {
                              System.out.println("resync");
                            }
                            sendMessage(MSG_RESYNCHRONIZE_RSP);
                            sFrameOutstandingPcb = MSG_RESYNCHRONIZE_RSP[2];
                            txSequenceNumber = 0;
                            rxSequenceNumber = 0;
                            ack = false;
                            poll = false;
                            errors = 0;
                            iFrameOutstanding = false;
                            sFrameOutstanding = false;
                            break;
                          case (byte) 0x97: // echo command
                            if (debug) {
                              System.out.println("echo");
                            }
                            int i = 0;
                            tx[i++] = (byte) 0x01;
                            tx[i++] = (byte) 0x00;
                            tx[i++] = (byte) 0xA7;
                            tx[i++] = (byte) (length / 256);
                            tx[i++] = (byte) (length % 256);
                            sendMessageWithChecksum(tx, i);
                            sendMessageWithChecksum(buffer, length);
                            sFrameOutstandingPcb = tx[2];
                            break;
                          case (byte) 0xA0: // resynchronise protocol response
                            sFrameOutstanding = false;
                            pstate = PSTATE_CONN;
                            break;
                          case (byte) 0xA6: // baud sync response
                            sFrameOutstanding = false;
                            pstate = PSTATE_INIT;
                            break;
                          case (byte) 0x91: // reset device command
                          case (byte) 0x92: // get communication parameters command
                          case (byte) 0x96: // baud sync command
                          default:
                            sendReject(pcb, (byte) 0x01); // unsupported S-frame command
                        }
                      case 0xC0: // R-Frame
                        if ((pcb & 0x20) == 0x20) {
                          poll = true;
                        } // send R-Frame
                        if (iFrameOutstanding && txSequenceNumber != (pcb & 0x01)) // rule 4.8
                        {
                          txSequenceNumber = (txSequenceNumber + 1) % 2;
                          iFrameOutstanding = false; // rule 4.9a
                          transmitQueue.remove();
                        }
                        break;
                      default: // Unsupported frame type
//System.out.println("Unsupported frame type"); // rule 6.4a ignore unsupported frame type
                        sendReject(pcb, (byte) 0x00); // rule 6.4b reject unsupported frame type
                        break;
                    }
                    message = true;
                  } else // rule 6.1a ignore frame EDC error
                  {
                    while (inputStream.available() > 0) {
                      rx = (byte) inputStream.read();
                      if (debug) {
                        System.out.println(state + " " + System.currentTimeMillis() + " < " + rx);
                      }
                    }
                    sendError(pcb, (byte) 0x01); // 0x01 = EDC or parity error
                    ack = false;
                    poll = false;
                  }
                  state = STATE_SA;
                  break;
              }
            }
          }
          time = System.currentTimeMillis();
          switch (pstate) {
            case PSTATE_SYNC:
              if (!sFrameOutstanding) {
                trials = 0;
              } else if (time > txacktime) // timeout
              {
                sFrameOutstanding = false;
                if (debug) {
                  System.out.println("timeout");
                }
                trials++;
                if (trials > 25) {
                  interrupt();
                }
              }
              if (!sFrameOutstanding) {
                if (debug) {
                  System.out.println("baudrate sync");
                }
                sendMessage(MSG_BAUD_SYNC);
                txacktime = System.currentTimeMillis() + 100;
                sFrameOutstandingPcb = MSG_BAUD_SYNC[2];
                sFrameOutstanding = true;
              }
              break;
            case PSTATE_INIT:
              if (!sFrameOutstanding) {
                trials = 0;
                txSequenceNumber = 0;
                rxSequenceNumber = 0;
                ack = false;
                poll = false;
                iFrameOutstanding = false; // rule 4.9c
              } else if (time > txacktime) // timeout
              {
                sFrameOutstanding = false;
                if (debug) {
                  System.out.println("timeout");
                }
                errors++;
                if (errors > 1) {
                  pstate = PSTATE_SYNC;
                }
              }
              if (!sFrameOutstanding) {
                if (debug) {
                  System.out.println("resync");
                }
                sendMessage(MSG_RESYNCHRONIZE_CMD);
                txacktime = System.currentTimeMillis() + BWT;
                sFrameOutstandingPcb = MSG_RESYNCHRONIZE_CMD[2];
                sFrameOutstanding = true;
              }
              break;
            case PSTATE_CONN:
            default:
              if (iFrameOutstanding && time > txacktime) // timeout
              {
                if (debug) {
                  System.out.println("timeout");
                }
                iFrameOutstanding = false; // rule 4.9b
                errors++;
                if (errors > 4) {
                  pstate = PSTATE_INIT;
                }
              }
              if (!iFrameOutstanding && !transmitQueue.isEmpty()) {
                if (debug) {
                  System.out.println("send i-frame");
                }
                byte[] data = (byte[]) transmitQueue.peek();
                int i = 0;
                tx[i++] = (byte) 0x01;
                tx[i++] = (byte) 0x00;
                tx[i++] = (byte) (0x20 ^ (2 * txSequenceNumber) ^ rxSequenceNumber);
                tx[i++] = (byte) (data.length / 256);
                tx[i++] = (byte) (data.length % 256);
                sendMessageWithChecksum(tx, i);
                sendMessageWithChecksum(data, data.length);
                txacktime = System.currentTimeMillis() + BWT;
                iFrameOutstanding = true;
                iFrameOutstandingPcb = tx[2];
                ack = false;
                poll = false;
              } else if (poll || (ack && time > rxacktime)) // rule 5.4a
              {
                if (debug) {
                  System.out.println("send ack");
                }
                int i = 0;
                tx[i++] = (byte) 0x01;
                tx[i++] = (byte) 0x00;
                tx[i++] = (byte) (0xC0 ^ rxSequenceNumber);
                tx[i++] = (byte) 0x00;
                tx[i++] = (byte) 0x00;
                sendMessageWithChecksum(tx, i);
                sendMessageWithChecksum(tx, 0);
                sFrameOutstandingPcb = tx[2];
                sFrameOutstanding = true;
                ack = false;
                poll = false;
              }
          }
          time = System.currentTimeMillis();
          long nextwakeup = time + 1000;
          if (ack && rxacktime < nextwakeup) {
            nextwakeup = rxacktime;
          }
          if ((sFrameOutstanding || iFrameOutstanding) && txacktime < nextwakeup) {
            nextwakeup = txacktime;
          }
          long waittime = 10 * ((nextwakeup - time) / 10);
          if (waittime <= 0) {
            waittime = 10;
          }
          try {
            wait(waittime);
          } catch (InterruptedException e) {
          }
        }
      }
    } catch (Exception e) {
//responseQueue.disable(new Exception("Could not receive data from serial port; " + e.getMessage()));
//notificationQueue.disable(new Exception("Could not receive data from serial port; " + e.getMessage()));
    }

  }
  /*
  0 RFU
  1 EDC or Parity Error
  2 Character Wait Timeout Error
   */

  public void sendError(byte pcb, byte reason) {
    if (debug) {
      System.out.println("error");
    }
    int i = 0;
    tx[i++] = (byte) 0x01;
    tx[i++] = (byte) 0x00;
    tx[i++] = (byte) 0x88;
    tx[i++] = (byte) 0x00;
    tx[i++] = (byte) 0x02;
    sendMessageWithChecksum(tx, i);
    i = 0;
    tx[i++] = pcb;
    tx[i++] = reason;
    sendMessageWithChecksum(tx, i);
    sFrameOutstandingPcb = tx[2];
    sFrameOutstanding = true;
  }

  public void sendMessageWithChecksum(byte[] msg, int length) {
    if (debug) {
      System.out.println(pstate + " " + System.currentTimeMillis() + " > " + msg);
    }
    try {
      byte checksum = 0;
      for (int i = 0; i < length; i++) {
        outputStream.write(msg[i]);
        checksum ^= msg[i];
      }
      outputStream.write(checksum);
      if (debug) {
        System.out.println(pstate + " " + System.currentTimeMillis() + " > " + checksum);
      }
      outputStream.flush();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void sendReject(byte pcb, byte reason) {
    if (debug) {
      System.out.println("reject");
    }
    int i = 0;
    tx[i++] = (byte) 0x01;
    tx[i++] = (byte) 0x00;
    tx[i++] = (byte) 0x85;
    tx[i++] = (byte) 0x00;
    tx[i++] = (byte) 0x02;
    sendMessageWithChecksum(tx, i);
    i = 0;
    tx[i++] = pcb;
    tx[i++] = reason;
    sendMessageWithChecksum(tx, i);
    sFrameOutstandingPcb = tx[2];
    sFrameOutstanding = true;
  }

  public void sendMessage(byte[] msg) {
    if (debug) {
      System.out.println(pstate + " " + System.currentTimeMillis() + " > " + msg);
    }
    try {
      outputStream.write(msg);
      outputStream.flush();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    Paslezer mtcr = new Paslezer();
    try {
      mtcr.ejectCard();
      while (!Paslezer.consumed) {
        System.out.println("Insert card plz");
        //Thread.sleep(1000);
        mtcr.consumeCard();
        Thread.sleep(1000);
      }

      String track3 = "null";
      while (track3.equals("null")) {
        track3 = mtcr.readTrack((byte) 0x03);
      }
      System.out.println("track 3 " + track3);
      String track2 = "null";
      while (track2.equals("null")) {
        track2 = mtcr.readTrack((byte) 0x02);
      }
      System.out.println("track 2 " + track2);

      String track1 = "null";
      while (track1.equals("null")) {
        track1 = mtcr.readTrack((byte) 0x01);
      }
      System.out.println("track 1 " + track1);


      mtcr.ejectCard();
    } catch (Exception e) {
    }
    System.out.println("--- THE END ---");
    System.exit(0);
  }
}
